Ícone Ver README do projeto

Introdução

A fatoração de matrizes é um algoritmo utilizado em sistemas de recomendação, ele desempenhou um papel importante no resultado da equipe ganhadora do Netflix Grand Prize (Zang et al, 2023). A decomposição de valor singular (SVD) é um método de fatoração de matrizes. Ela ajuda na redução de dimensionalidade, sem eliminar as fontes de informações (MINER et al., 2012).

A fórmula da decomposição de valor singular (SVD) é:
$$M = \sum V^{t}$$


Onde,
$M$ é a matriz original que queremos decompor;
$U$ é matriz singular esquerda (colunas são vetores singulares esquerdos). Colunas U contêm autovetores da matriz $MM^{t}$
$\sum$ é uma matriz diagonal contendo valores singulares (próprios)
$V$ é matriz singular direita (colunas são vetores singulares direitos). $V$ colunas contêm autovetores da matriz $M^{t}M$


Fonte da imagem: Wikipedia

%load_ext pretty_jupyter

Importar bibliotecas

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt


from sklearn.metrics import confusion_matrix

# confusion_matrix, accuracy_score, precision_score, 
#   recall_score, f1_score: Funções do scikit-learn 
#   para calcular métricas de desempenho.
from sklearn.metrics import confusion_matrix, accuracy_score, precision_score, recall_score, f1_score
from sklearn.model_selection import GridSearchCV
from sklearn.ensemble import BaggingClassifier
import time
from tqdm import tqdm
from sklearn.base import clone
from sklearn.decomposition import TruncatedSVD
from sklearn.metrics.pairwise import cosine_similarity
from sklearn.preprocessing import StandardScaler
import joblib

Carregar arquivos

Tabela de apoio

catalago = pd.read_pickle("C:/0.Projetos/5.Sistema_de_Recomendacao_MovieLens_2/Datasets/3.Datasets_Transformação/3.3_Datasets_Transformação_parte_3/catalogo.pickle", compression='gzip')


Tabelas para modelagem 1: filmes

# Dados de treino
knn_filmes_treino = pd.read_pickle("C:/0.Projetos/5.Sistema_de_Recomendacao_MovieLens_2/Datasets/3.Datasets_Transformação/3.3_Datasets_Transformação_parte_3/knn_filmes_treino.pickle", compression='gzip')
# Dados de teste
knn_filmes_teste = pd.read_pickle("C:/0.Projetos/5.Sistema_de_Recomendacao_MovieLens_2/Datasets/3.Datasets_Transformação/3.3_Datasets_Transformação_parte_3/knn_filmes_teste.pickle", compression='gzip')
# Olhar tabela de treino para modelagem 1: Filmes
knn_filmes_treino
title (2019) "Great Performances" Cats (1998) #Alive (2020) #Female Pleasure (2018) #Iamhere (2020) #UNFIT: The Psychology of Donald Trump (2019) $ (Dollars) (1971) $5 a Day (2008) $9.99 (2008) $ellebrity (Sellebrity) (2012) ... Üvegtigris (2001) Τέλειοι Ξένοι (2016) Χούλιγκανς: Κάτω τα χέρια απ' τα νιάτα! (1983) Делай - раз! (1989) Каменная башка (2008) Карусель (1970) Он вам не Димон (2017) Пес Барбос и необычный кросс (1961) Я худею (2018) …And the Fifth Horseman Is Fear (1965)
userId
5 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
15 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
49 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
119 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
134 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
330651 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
330661 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
330811 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
330949 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
330963 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0

7943 rows × 24326 columns

# Olhar tabela de teste para modelagem 1: Filmes
knn_filmes_teste
title #Alive (2020) $ (Dollars) (1971) '71 (2014) '83 (2021) 'Hellboy': The Seeds of Creation (2004) 'Round Midnight (1986) 'Salem's Lot (2004) 'Til There Was You (1997) 'burbs, The (1989) 'night Mother (1986) ... tick, tick...BOOM! (2021) xXx (2002) xXx: Return of Xander Cage (2017) xXx: State of the Union (2005) ¡Three Amigos! (1986) ¿Quién mató a Bambi? (2013) À nous la liberté (Freedom for Us) (1931) Ánimas (2018) Épouse-moi mon pote (2017) Ужас, который всегда с тобой (2007)
userId
128 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
172 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
465 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
598 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
919 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
330236 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
330321 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
330496 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
330667 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
330948 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0

1986 rows × 15496 columns

Modelagem

Treinar o SVD

# Aplicar SVD
n_components = 20  
svd = TruncatedSVD(n_components=n_components) # Reduzir a dimensionalidade
latent_matrix = svd.fit_transform(knn_filmes_treino) # Treinar o SVD
latent_matrix_transpose = svd.components_

# Reconstruir a matriz de avaliações aproximada
reconstructed_matrix = np.dot(latent_matrix, latent_matrix_transpose)

# Similaridade entre usuários
user_similarity = cosine_similarity(latent_matrix)
user_similarity
array([[ 1.        ,  0.88488888,  0.83560888, ...,  0.13263455,
         0.51287916,  0.25578198],
       [ 0.88488888,  1.        ,  0.73380236, ..., -0.04176017,
         0.68229894,  0.1340374 ],
       [ 0.83560888,  0.73380236,  1.        , ...,  0.03963122,
         0.41904029,  0.2059464 ],
       ...,
       [ 0.13263455, -0.04176017,  0.03963122, ...,  1.        ,
         0.11863279,  0.06117318],
       [ 0.51287916,  0.68229894,  0.41904029, ...,  0.11863279,
         1.        ,  0.36283311],
       [ 0.25578198,  0.1340374 ,  0.2059464 , ...,  0.06117318,
         0.36283311,  1.        ]])
knn_filmes_teste.value_counts().sum()
np.int64(1986)

Fazer a previsão para um userId

# Exemplo de função ajustada para usar .iloc se knn_filmes_teste é um DataFrame do Pandas
def predict_rating(user_index, item_index, user_similarity, knn_filmes_teste, knn_filmes_treino):
    # Se o usuário já avaliou o filme, retorna a avaliação real
    if knn_filmes_teste.iloc[user_index, item_index] != 0:
        return knn_filmes_teste.iloc[user_index, item_index]
    
    # Calcula a previsão com base na similaridade entre usuários
    predicted_rating = np.dot(user_similarity[user_index], knn_filmes_treino.iloc[:, item_index]) / np.sum(user_similarity[user_index])
    
    return predicted_rating

# Exemplo de uso
user_index = 128
item_index = 3

predicted_rating = predict_rating(user_index, item_index, user_similarity, knn_filmes_teste, knn_filmes_treino)
print(f"Predicted rating for user {user_index} on item {item_index}: {predicted_rating}")
Predicted rating for user 128 on item 3: 0.0001792284916849959

Fazer a previsão para todos userId

def predict_all_ratings(user_similarity, knn_filmes_teste, knn_filmes_treino):
    predicted_ratings = knn_filmes_teste.copy()  # Copia o DataFrame knn_filmes_teste para armazenar as previsões
    
    for user_index in range(knn_filmes_teste.shape[0]):
        # Encontra os índices dos filmes não avaliados pelo usuário atual
        unrated_indices = np.where(knn_filmes_teste.iloc[user_index, :] == 0)[0]
        
        # Calcula as previsões para os filmes não avaliados
        if len(unrated_indices) > 0:
            user_sim = user_similarity[user_index]
            knn_treino_subset = knn_filmes_treino.iloc[:, unrated_indices]
            predicted_ratings.iloc[user_index, unrated_indices] = np.dot(user_sim, knn_treino_subset) / np.sum(user_sim)
    
    return predicted_ratings
from tqdm import tqdm
import time

# Número total de iterações
total_iterations = 100

# Uso do tqdm para criar a barra de progresso
for i in tqdm(range(total_iterations)):
    # Simulação de trabalho
    #time.sleep(0.1)  # Aqui você substitui pelo seu trecho de código
    # Chamada da função para prever todas as avaliações
    predicted_ratings_df = predict_all_ratings(user_similarity, knn_filmes_teste, knn_filmes_treino)


# Ao finalizar, a barra de progresso será completa
print("Execução completa!")
Execução completa!
# Tabela com as recomendações para todos os usuários
predicted_ratings_df
title #Alive (2020) $ (Dollars) (1971) '71 (2014) '83 (2021) 'Hellboy': The Seeds of Creation (2004) 'Round Midnight (1986) 'Salem's Lot (2004) 'Til There Was You (1997) 'burbs, The (1989) 'night Mother (1986) ... tick, tick...BOOM! (2021) xXx (2002) xXx: Return of Xander Cage (2017) xXx: State of the Union (2005) ¡Three Amigos! (1986) ¿Quién mató a Bambi? (2013) À nous la liberté (Freedom for Us) (1931) Ánimas (2018) Épouse-moi mon pote (2017) Ужас, который всегда с тобой (2007)
userId
128 0.000149 0.000142 0.000910 0.000098 0.000135 0.000168 0.000049 0.000013 0.000201 0.000017 ... 0.011577 0.002151 0.000635 0.000461 0.000299 0.000135 0.000020 0.000246 0.000055 0.000048
172 0.000117 0.000134 0.001026 0.000150 0.000126 0.000148 0.000032 0.000004 0.000271 0.000010 ... 0.014429 0.002467 0.000753 0.000597 0.000287 0.000126 0.000019 0.000338 0.000064 0.000047
465 0.000160 0.000239 0.000765 0.000144 0.000191 0.000164 0.000025 0.000005 0.000222 0.000015 ... 0.014693 0.002695 0.000790 0.000562 0.000283 0.000098 0.000026 0.000299 0.000073 0.000044
598 0.000447 0.000466 0.000902 0.000153 0.000454 0.000392 0.000065 0.000113 0.000187 0.000022 ... 0.016987 0.005590 0.001436 0.000401 0.000710 -0.000008 0.000066 0.000262 0.000121 0.000127
919 0.000176 0.000175 0.001303 0.000239 0.000188 0.000205 0.000031 0.000024 0.000323 0.000016 ... 0.019631 0.003365 0.001053 0.000744 0.000325 0.000204 0.000025 0.000331 0.000072 0.000056
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
330236 0.000109 0.000296 0.000910 0.000143 0.000202 0.000313 0.000077 0.000053 0.000185 0.000020 ... 0.011580 0.002583 0.000709 0.000253 0.000543 0.000056 0.000025 0.000289 0.000053 0.000079
330321 -0.000012 0.000312 0.001057 0.000228 0.000204 0.000195 0.000027 0.000014 0.000258 0.000009 ... 0.020154 0.003299 0.001039 0.000770 0.000293 0.000126 0.000026 0.000294 0.000067 0.000049
330496 0.000175 0.000254 0.001294 0.000161 0.000214 0.000262 0.000074 0.000029 0.000232 0.000012 ... 0.015922 0.002862 0.000777 0.000559 0.000436 0.000128 0.000025 0.000392 0.000070 0.000070
330667 0.000129 0.000206 0.000991 0.000204 0.000152 0.000219 0.000084 0.000036 0.000313 0.000030 ... 0.013308 0.002471 0.000709 0.000489 0.000457 0.000096 0.000027 0.000357 0.000072 0.000073
330948 -0.000103 0.000044 0.000511 0.000048 0.000074 0.000154 0.000018 0.000019 0.000175 0.000019 ... 0.011471 0.001815 0.000643 0.000453 0.000268 0.000136 0.000012 0.000084 0.000020 0.000041

1986 rows × 15496 columns

# Salvar tabela predicted_ratings_df
predicted_ratings_df.to_pickle("C:/0.Projetos/5.Sistema_de_Recomendacao_MovieLens_2/Datasets/6.Modelagem_SVD/predicted_ratings_df.pickle", compression="gzip")


Listagem de recomendações

Criar um dataframe com 2 colunas: UserId e uma lista com as recomendações.

n_recomendacoes = 3

# Função para encontrar os top N filmes recomendados para um usuário
def top_recomendacoes(row, n) -> list:
    ''' Função que encontra as melhores recomendações para cada userId
    Args:
      - row = linha do DataFrame. 
              Cada linha representa as recomendações de filme para cada userId.
      - n = número de filmes a serem recomendados 

    Return:
      - Retorna uma lista com os "n" valores mais altos em cada. 
       Ou seja, retorna com as "n" recomendações de filmes.      
    '''
    # Selecionar e retornar os "n" valores mais altos de cada linha
    return row.nlargest(n).index.tolist()

    #OBS: .index(): Obtemos os nomes dos filmes, ao invés dos valores.
    #     .tolist(): Criamos uma lista com os nomes dos filmes 
# Aplicar a função a cada linha do DataFrame de previsões
recomendacoes = predicted_ratings_df.apply(top_recomendacoes, n=n_recomendacoes, axis=1) 

# OBS: apply(... axis=1) -> Aplicar em cada linha
#      n = número de recomendações
recomendacoes
userId
128       [The Croods: A New Age (2020), Jungle (2017), ...
172       [The Croods: A New Age (2020), Jungle (2017), ...
465       [10 Things I Hate About You (1999), 21 Jump St...
598       [Lady in Red, The (1979), The Croods: A New Ag...
919       [The Croods: A New Age (2020), Pianist, The (2...
                                ...                        
330236    [The Croods: A New Age (2020), Lady in Red, Th...
330321    [The Croods: A New Age (2020), Pianist, The (2...
330496    [Event Horizon (1997), High Fidelity (2000), R...
330667    [2001: A Space Odyssey (1968), Blade Runner (1...
330948    [Bad Santa (2003), Due Date (2010), Fast and t...
Length: 1986, dtype: object
# Criar um DataFrame para armazenar as recomendações
df_recomendacoes = pd.DataFrame(recomendacoes.tolist(), index=recomendacoes.index, columns=[f"recomendação_{i+1}" for i in range(n_recomendacoes)])
# Transformar o userId em coluna
df_recomendacoes = df_recomendacoes.reset_index()
df_recomendacoes
userId recomendação_1 recomendação_2 recomendação_3
0 128 The Croods: A New Age (2020) Jungle (2017) Pianist, The (2002)
1 172 The Croods: A New Age (2020) Jungle (2017) Pianist, The (2002)
2 465 10 Things I Hate About You (1999) 21 Jump Street (2012) 40-Year-Old Virgin, The (2005)
3 598 Lady in Red, The (1979) The Croods: A New Age (2020) Pianist, The (2002)
4 919 The Croods: A New Age (2020) Pianist, The (2002) Jungle (2017)
... ... ... ... ...
1981 330236 The Croods: A New Age (2020) Lady in Red, The (1979) Jungle (2017)
1982 330321 The Croods: A New Age (2020) Pianist, The (2002) Juno (2007)
1983 330496 Event Horizon (1997) High Fidelity (2000) Royal Tenenbaums, The (2001)
1984 330667 2001: A Space Odyssey (1968) Blade Runner (1982) Boyhood (2014)
1985 330948 Bad Santa (2003) Due Date (2010) Fast and the Furious: Tokyo Drift, The (Fast a...

1986 rows × 4 columns

df_recomendacoes.isna().sum()
userId            0
recomendação_1    0
recomendação_2    0
recomendação_3    0
dtype: int64


Verificar se o userId assistiu a recomendação

Vamos criar uma função chamada assistiu_ou_nao. Esta função vai verificar se os usuários assistiram ou não a alguma das recomendações e em seguida vai retornar 1 (se assistiu), 0 (se não assitiu) e 2 (se o filmes não está no conjunto de teste).

def assistiu_ou_nao(row, df_teste):
    # Obtém as recomendações para o usuário atual
    recomendacoes = row.drop('userId').values
    
    # Verifica se algum dos filmes recomendados está no df_teste e foi assistido (rating diferente de 0)
    for filme in recomendacoes:
        if filme in df_teste.columns and df_teste.loc[row['userId'], filme] != 0:
            return 1
        elif filme not in df_teste.columns:
            return 2
        #else:
        #    return 0
    # Se nenhum filme recomendado foi assistido, retorna 0
    return 0    
# Aplicar a função a cada linha do DataFrame de recomendações para criar a coluna dummy
df_recomendacoes['assistiu_recomendacao'] = df_recomendacoes.apply(assistiu_ou_nao, df_teste=knn_filmes_teste, axis=1)
df_recomendacoes
userId recomendação_1 recomendação_2 recomendação_3 assistiu_recomendacao
0 128 The Croods: A New Age (2020) Jungle (2017) Pianist, The (2002) 0
1 172 The Croods: A New Age (2020) Jungle (2017) Pianist, The (2002) 0
2 465 10 Things I Hate About You (1999) 21 Jump Street (2012) 40-Year-Old Virgin, The (2005) 1
3 598 Lady in Red, The (1979) The Croods: A New Age (2020) Pianist, The (2002) 0
4 919 The Croods: A New Age (2020) Pianist, The (2002) Jungle (2017) 0
... ... ... ... ... ...
1981 330236 The Croods: A New Age (2020) Lady in Red, The (1979) Jungle (2017) 0
1982 330321 The Croods: A New Age (2020) Pianist, The (2002) Juno (2007) 1
1983 330496 Event Horizon (1997) High Fidelity (2000) Royal Tenenbaums, The (2001) 1
1984 330667 2001: A Space Odyssey (1968) Blade Runner (1982) Boyhood (2014) 1
1985 330948 Bad Santa (2003) Due Date (2010) Fast and the Furious: Tokyo Drift, The (Fast a... 1

1986 rows × 5 columns

Resultados Modelo 1

Na etapa anterior, geramos as recomendações, e criamos uma coluna (assistiu_recomendacao) para comparar as recomendações com os filmes assisitidos pelos userId dos dados de teste. Cada linha desta coluna é representada por 0, 1 ou 2.

  • 0: significa que nenhum filme da recomendação foi assistido pelo usuário ;
  • 1: significa que pelo menos um dos filmes recomendados foi assistido pelo usuário;
  • 2: significa que o filme recomendado, não está nos dados de teste. A explicação disso foi dada no tópico acima.
df_recomendacoes['assistiu_recomendacao'].value_counts()
assistiu_recomendacao
1    1078
0     908
Name: count, dtype: int64

)



Ícone Ver README do projeto

Referência Bibliográfica

MINER, G. et al. (EDS.). Chapter 11 - Singular Value Decomposition in Text Mining.
Disponível em: https://www.sciencedirect.com/science/article/pii/B9780123869791000396

ZHANG, A. et al.Dive into deep learning. Cambridge, England: Cambridge University Press, 2023.
Disponível em: https://d2l.ai/chapter_recommender-systems/mf.html